package mqtt

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"time"

	mc "gitee.com/shipnet/flogo-common/mqtt"
	mqtt "github.com/eclipse/paho.mqtt.golang"
	"github.com/project-flogo/core/activity"
	"github.com/project-flogo/core/data/coerce"
	"github.com/project-flogo/core/data/metadata"
	"github.com/project-flogo/core/support/ssl"
)

var activityMd = activity.ToMetadata(&Settings{}, &Input{}, &Output{})

func init() {
	_ = activity.Register(&Activity{}, New)
}

// New create new activity
func New(ctx activity.InitContext) (activity.Activity, error) {
	settings := &Settings{}
	err := metadata.MapToStruct(ctx.Settings(), settings, true)
	if err != nil {
		return nil, err
	}

	act := Activity{settings: settings}

	opts, err := act.initClientOption(settings)
	if err != nil {
		return nil, err
	}
	opts.Name = settings.Name
	mclient, err := mc.NewMQTT(*opts)
	if err != nil {
		return nil, err
	}

	act.client = mclient
	return &act, nil
}

// Activity activity
type Activity struct {
	settings *Settings
	client   mc.Client
}

// Metadata return metadata
func (a *Activity) Metadata() *activity.Metadata {
	return activityMd
}

// Eval evaluate activity
func (a *Activity) Eval(ctx activity.Context) (done bool, err error) {

	input := &Input{}

	err = ctx.GetInputObject(input)

	if err != nil {
		return true, err
	}

	topic := input.Topic
	qos := input.Qos
	retained := input.IsRetained
	payload := input.Payload
	err = a.client.Publish(topic, mc.QOS(qos), retained, time.Second*10, payload)

	ctx.Logger().Debugf("Publish Message: topic = %s, payload = %v, err = %v", topic, payload, err)

	errStr := ""
	if err != nil {
		errStr = err.Error()
	}
	output := &Output{
		Err: errStr,
	}
	err = ctx.SetOutputObject(output)
	return true, err
}

var defaultKeepAlive = time.Second * 60

func (a *Activity) initClientOption(settings *Settings) (*mc.Settings, error) {

	opts := mc.Settings{}
	if len(settings.Servers) == 0 {
		return nil, fmt.Errorf("invalid settings, servers are required")
	}
	servers := []string{}
	for _, val := range settings.Servers {
		s, err := coerce.ToString(val)
		if err != nil {
			return nil, fmt.Errorf("invalid settings, server url should be string")
		}
		st := strings.TrimSpace(s)
		if st == "" {
			return nil, fmt.Errorf("invalid settings, server url should not be empty")
		}
		servers = append(servers, s)
	}
	opts.Servers = servers

	opts.ClientID = settings.ClientID
	opts.Username = settings.Username
	opts.Password = settings.Password
	opts.CleanSession = settings.CleanSession
	opts.Order = settings.Order
	opts.WillEnabled = settings.WillEnabled
	opts.WillQos = settings.WillQos
	opts.WillRetained = settings.WillRetained
	opts.WillTopic = settings.WillTopic

	if settings.WillEnabled {
		opts.WillEnabled = settings.WillEnabled
		opts.WillQos = settings.WillQos
		opts.WillRetained = settings.WillRetained
		opts.WillTopic = settings.WillTopic

		payload := make([]byte, len(settings.WillPayload))
		for idx, val := range settings.WillPayload {
			bi, err := coerceToInt(val)
			if err != nil {
				return nil, fmt.Errorf("invalid last will payload: %v", settings.WillPayload)
			}
			b := byte(bi)
			payload[idx] = b
		}
		opts.WillPayload = payload
	}

	opts.ProtocolVersion = settings.ProtocolVersion

	keepAliveStr := strings.TrimSpace(settings.KeepAlive)
	if keepAliveStr != "" {
		keepAlive, err := time.ParseDuration(keepAliveStr)
		if err != nil {
			return nil, fmt.Errorf("invalid settings, invalid KeepAlive duration")
		}
		opts.KeepAlive = keepAlive
	} else {
		opts.KeepAlive = defaultKeepAlive
	}

	pingTimeoutStr := strings.TrimSpace(settings.PingTimeout)
	if pingTimeoutStr != "" {
		pingTimeout, err := time.ParseDuration(pingTimeoutStr)
		if err != nil {
			return nil, fmt.Errorf("invalid settings, invalid PingTimeout duration")
		}
		opts.PingTimeout = pingTimeout
	}

	connectTimeoutStr := strings.TrimSpace(settings.ConnectTimeout)
	if connectTimeoutStr != "" {
		connectTimeout, err := time.ParseDuration(connectTimeoutStr)
		if err != nil {
			return nil, fmt.Errorf("invalid settings, invalid ConnectTimeout duration")
		}
		opts.ConnectTimeout = connectTimeout
	}

	maxConnectIntervalStr := strings.TrimSpace(settings.MaxReconnectInterval)
	if maxConnectIntervalStr != "" {
		maxConnectInterval, err := time.ParseDuration(settings.MaxReconnectInterval)
		if err != nil {
			return nil, fmt.Errorf("invalid settings, invalid MaxConnectInterval duration")
		}
		opts.MaxReconnectInterval = maxConnectInterval
	}

	opts.AutoReconnect = settings.AutoReconnect

	if settings.Store != ":memory:" && settings.Store != "" {
		store := mqtt.NewFileStore(settings.Store)
		opts.Store = store
	}

	writeTimeoutStr := settings.WriteTimeout
	if writeTimeoutStr != "" {
		writeTimeout, err := time.ParseDuration(writeTimeoutStr)
		if err != nil {
			return nil, fmt.Errorf("invalid settings, invalid WriteTimeout duration")
		}
		opts.WriteTimeout = writeTimeout
	}

	opts.ResumeSubs = settings.ResumeSubs

	if len(settings.HTTPHeaders) > 0 {
		headers := map[string][]string{}
		for k, v := range settings.HTTPHeaders {
			headers[k] = []string{v}
		}
		opts.HTTPHeaders = headers
	}

	opts.DefaultPublishHandler = a.genDefaultPublishHandler()
	opts.OnConnectHandler = a.genOnConnectHandler()
	opts.ConnectionLostHandler = a.genConnectionLostHandler()

	if len(settings.TLSConfig) != 0 {
		cfg := &ssl.Config{}
		err := cfg.FromMap(settings.TLSConfig)
		if err != nil {
			return nil, fmt.Errorf("invalid settings, invalid tls settings")
		}

		if _, set := settings.TLSConfig["skipVerify"]; !set {
			cfg.SkipVerify = true
		}
		if _, set := settings.TLSConfig["useSystemCert"]; !set {
			cfg.UseSystemCert = true
		}
		tlsConfig, err := ssl.NewClientTLSConfig(cfg)
		if err != nil {
			return nil, err
		}
		opts.TLSConfig = tlsConfig
	}

	return &opts, nil
}

func (a *Activity) genDefaultPublishHandler() mqtt.MessageHandler {
	return nil
}

func (a *Activity) genOnConnectHandler() mqtt.OnConnectHandler {
	return nil
}

func (a *Activity) genConnectionLostHandler() mqtt.ConnectionLostHandler {
	return nil
}

func coerceToInt(val interface{}) (int, error) {
	switch t := val.(type) {
	case uint:
		return int(t), nil
	case int:
		return t, nil
	case int8:
		return int(t), nil
	case uint8:
		return int(t), nil
	case int16:
		return int(t), nil
	case uint16:
		return int(t), nil
	case int32:
		return int(t), nil
	case uint32:
		return int(t), nil
	case uint64:
		return int(t), nil
	case int64:
		return int(t), nil
	case float32:
		return int(t), nil
	case float64:
		return int(t), nil
	case json.Number:
		i, err := t.Int64()
		return int(i), err
	case string:
		return strconv.Atoi(t)
	case bool:
		if t {
			return 1, nil
		}
		return 0, nil
	case nil:
		return 0, nil
	default:
		return 0, fmt.Errorf("unable to coerce %#v to int", val)
	}
}
